﻿//	Copyright (c) 2012 Jonathan Loe
//
//	MIT license (http://en.wikipedia.org/wiki/MIT_License)
//
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights 
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
//	of the Software, and to permit persons to whom the Software is furnished to do so, 
//	subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in all 
//	copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
//	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
//	PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
//	FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
//	ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Text;
using System.Transactions;

using Ordinaire.Db;
using Ordinaire.Security;
using Ordinaire.Taxonomy.Data;

namespace Ordinaire.Taxonomy
{
    /// <summary>
    /// Organises entity/item with tagging.
    /// </summary>
    public class EntityOrganiser : BusinessManager<TagDataManager, OContext>
    {
        #region Variables

        /// <summary>
        /// Reference to collection of Vocabularies in data source.
        /// </summary>
        private Vocabularies vocabularies;

        #endregion

        #region Constructor

        /// <summary>
        /// Returns new instance of Ordinaire.Taxonomy.EntityOrganiser.
        /// </summary>
        public EntityOrganiser()
        {
            dataManager = new TagDataManager();
            vocabularies   = new Vocabularies();
        }

        #endregion

        #region Property methods

        /// <summary>
        /// Gets or sets the data context currently assigned.
        /// </summary>
        public override OContext Context
        {
            get { return dataManager.Context; }
            set
            {
                dataManager.Context  = value;
                vocabularies.Context = value;
            }
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Clears all keywords tagged to the specified item.
        /// </summary>
        /// <param name="item">the item which the tag keywords associated to</param>
        public void ClearTags(Entity<int> item)
        {
            ClearTags<Tags>(item);
        }

        /// <summary>
        /// Clears all keywords tagged to the specified item.
        /// </summary>
        /// <typeparam name="TTag">the table type to use in the data source</typeparam>
        /// <param name="item">the item which the tag keywords associated to</param>
        public void ClearTags<TTag>(Entity<int> item)
            where TTag : Tags
        {
            item.ThrowIfNull<ArgumentNullException>("item");
            dataManager.DeleteAllByEntity<TTag>(item);
        }

        /// <summary>
        /// Returns all tag keywords withs the specified item/entity.
        /// </summary>
        /// <param name="item">the item/entity</param>
        /// <returns>all tag keywords</returns>
        public List<string> GetTags(Entity<int> item)
        {
            item.ThrowIfNull<ArgumentNullException>("item");
            item.Id.ThrowIfEmpty<InvalidOperationException>(String.Format(ExceptionMessage.Invalid, "item's id"));

            List<Tags> data = dataManager.GetAllByEntity(item);

            return data.AsEnumerable()
                .Select(d => d.Foreign.Name)
                .Distinct()
                .ToList();
        }

        /// <summary>
        /// Returns all entities tagged with the specified keyword
        /// </summary>
        /// <param name="keyword">the tag keyword</param>
        /// <returns>list of elements tagged with the specified keyword</returns>
        public List<Entity<int>> GetEntitiesByTag(string keyword)
        {
            return GetEntitiesByTagInTable<Tags>(keyword);
        }

        /// <summary>
        /// Returns all entities tagged with the specified keyword from the specified table type in the data source.
        /// </summary>
        /// <typeparam name="TTag">the table type to use in the data source</typeparam>
        /// <param name="keyword">the tag keyword</param>
        /// <returns>list of elements tagged with the specified keyword</returns>
        public List<Entity<int>> GetEntitiesByTagInTable<TTag>(string keyword)
            where TTag : Tags
        {
            keyword.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "keyword"));

            List<Entity<int>> list = new List<Entity<int>>();
            Vocab vocab = vocabularies.Create(keyword);

            List<TTag> data = dataManager.GetAllByVocabId<TTag>(vocab.Id);
            if (data.Count == 0)
            {
                return list;
            }

            for (int i = 0; i < data.Count; i++)
            {
                Tags record = data[i];
                Entity<int> item = (Entity<int>) record.EntityType.Instance(record.EntityAssembly);
                item.Id = record.EntityId;
                list.Add(item);
            }

            return list;
        }

        /// <summary>
        /// Returns all entities of specified type T tagged with the specified keyword.
        /// </summary>
        /// <typeparam name="TEntity">the element type</typeparam>
        /// <param name="keyword">the tag keyword</param>
        /// <returns>list of elements tagged with the specified keyword</returns>
        public List<TEntity> GetEntitiesByTag<TEntity>(string keyword)
            where TEntity : Entity<int>
        {
            return GetEntitiesByTag<TEntity, Tags>(keyword);
        }

        /// <summary>
        /// Returns all entities of specified element type tagged with the specified keyword from specified table type.
        /// </summary>
        /// <typeparam name="TEntity">the element type</typeparam>
        /// <typeparam name="TTable">the table type to use in the data source</typeparam>
        /// <param name="keyword">the tag keyword</param>
        /// <returns>list of elements tagged with the specified keyword</returns>
        public List<TEntity> GetEntitiesByTag<TEntity, TTable>(string keyword)
            where TEntity : Entity<int>
            where TTable  : Tags
        {
            keyword.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "keyword"));

            List<TEntity> list = new List<TEntity>();
            Vocab vocab = vocabularies.Create(keyword);

            List<Tags> data = dataManager.GetAllByVocabIdAndEntityType<TTable>(vocab.Id, typeof(TEntity));
            if (data.Count == 0)
            {
                return list;
            }

            for (int i = 0; i < data.Count; i++)
            {
                Tags record = data[i];
                TEntity item = typeof(TEntity).Instance<TEntity>();
                item.Id = record.EntityId;
                list.Add(item);
            }

            return list;
        }

        /// <summary>
        /// Returns a value indicating whether the specified tag keyword is already assigned to the specified item.
        /// </summary>
        /// <param name="item">the item to test</param>
        /// <param name="keyword">the tag keyword</param>
        /// <returns>true if exist; otherwise false</returns>
        public bool IsTagged(Entity<int> item, string keyword)
        {
            return IsTagged<Tags>(item, keyword);
        }

        /// <summary>
        /// Returns a value indicating whether the specified tag keyword is already assigned to the specified item.
        /// </summary>
        /// <typeparam name="TTag">the table type to use in the data source</typeparam>
        /// <param name="item">the item to test</param>
        /// <param name="keyword">the tag keyword</param>
        /// <returns>true if exist; otherwise false</returns>
        public bool IsTagged<TTag>(Entity<int> item, string keyword)
            where TTag : Tags
        {
            item.ThrowIfNull<ArgumentNullException>("item");
            keyword.ThrowIfNullOrEmpty<ArgumentException>(String.Format(ExceptionMessage.IsNullOrEmpty, "keyword"));

            Vocab vocab = vocabularies.Create(keyword);
            return TagExists<TTag>(item, vocab.Id);
        }

        /// <summary>
        /// Clears existing item tags and tags the specified item with the specified list of keywords.
        /// </summary>
        /// <param name="item">the item to be tagged</param>
        /// <param name="keywords">the tag keywords</param>
        /// <returns>true if successful; otherwise false</returns>
        public bool Retag(Entity<int> item, params string[] keywords)
        {
            return Retag<Tags>(item, keywords);
        }

        /// <summary>
        /// Clears existing item tags and tags the specified item with the specified list of keywords.
        /// </summary>
        /// <typeparam name="TTag">the table type to use in the data source</typeparam>
        /// <param name="item">the item to be tagged</param>
        /// <param name="keywords">the tag keywords</param>
        /// <returns>true if successful; otherwise false</returns>
        public bool Retag<TTag>(Entity<int> item, params string[] keywords)
            where TTag : Tags
        {
            item.ThrowIfNull<ArgumentNullException>("item");
            keywords.ThrowIfNull<ArgumentNullException>("keywords");
            keywords.ThrowIfEmpty<ArgumentException>("keywords is empty");

            using (TransactionScope transaction = new TransactionScope())
            {
                ClearTags<TTag>(item);

                // share same DataContext to prevent the need of activating MSDTC during TransactionScope.
                vocabularies.Context = dataManager.Context;

                bool success = Tag<TTag>(item, keywords);
                
                if (success)
                {
                    transaction.Complete();
                }

                return success;
            }
        }

        /// <summary>
        /// Tags or labels the specified item with the specified keyword(s).
        /// </summary>
        /// <param name="item">the item to be tagged</param>
        /// <param name="keywords">the tag keywords</param>
        /// <returns>true if successful; otherwise false</returns>
        public bool Tag(Entity<int> item, params string[] keywords)
        {
            return Tag<Tags>(item, keywords);
        }

        /// <summary>
        /// Tags or labels the specified item with the specified keyword(s).
        /// </summary>
        /// <typeparam name="TTag">the tag table to use in the data source</typeparam>
        /// <param name="item">the item to be tagged</param>
        /// <param name="keywords">the tag keywords</param>
        /// <returns>true if successful; otherwise false</returns>
        public bool Tag<TTag>(Entity<int> item, params string[] keywords)
            where TTag : Tags
        {
            item.ThrowIfNull<ArgumentNullException>("item");
            keywords.ThrowIfNull<ArgumentNullException>("keywords");
            keywords.ThrowIfEmpty<ArgumentException>("keywords is empty");

            string keyword = String.Empty;

            using (TransactionScope transaction = new TransactionScope())
            {
                for (int i = 0; i < keywords.Length; i++)
                {
                    keyword = keywords[i];
                    Vocab vocab = vocabularies.Create(keyword);

                    // share same DataContext to prevent the need of activating MSDTC during TransactionScope.
                    dataManager.Context = vocabularies.Context;

                    // ignore if keyword already exists
                    if (TagExists(item, vocab.Id))
                    {
                        continue;
                    }
                
                    TTag tagData           = typeof(TTag).Instance<TTag>();
                    tagData.EntityAssembly = item.GetType().AssemblyQualifiedName;
                    tagData.EntityType     = item.GetType().FullName;
                    tagData.EntityId       = item.Id;
                    tagData.ForeignId        = vocab.Id;

                    dataManager.Insert<TTag, int, string>(tagData);
                }

                transaction.Complete();
            }

            return true;
        }

        #endregion

        #region Protected methods

        /// <summary>
        /// Returns a value indicating whether the specified vocab id is already assigned to the specified item.
        /// </summary>
        /// <param name="item">the item to test</param>
        /// <param name="vocabId">the vocabulary id</param>
        /// <returns>true if exist; otherwise false</returns>
        protected bool TagExists(Entity<int> item, int vocabId)
        {
            return TagExists<Tags>(item, vocabId);
        }

        /// <summary>
        /// Returns a value indicating whether the specified vocab id is already assigned to the specified item.
        /// </summary>
        /// <typeparam name="TTable">the table type to use in the data source</typeparam>
        /// <param name="item">the item to test</param>
        /// <param name="vocabId">the vocabulary id</param>
        /// <returns>true if exist; otherwise false</returns>
        protected bool TagExists<TTable>(Entity<int> item, int vocabId)
            where TTable : Tags
        {
            item.ThrowIfNull<ArgumentNullException>("item");
            item.Id.ThrowIfEmpty<InvalidOperationException>(String.Format(ExceptionMessage.Invalid, "item's id"));
            vocabId.ThrowIfEmpty<ArgumentException>(String.Format(ExceptionMessage.Invalid, "vocabId"));

            return dataManager.Exists<TTable>(item, vocabId);
        }

        #endregion
    }
}
